import os, sys
import time
import threading
from functools import wraps, partial
from wsgiref.util import request_uri
import cgi
from genshi.template import TemplateLoader
import memento
import static
from flup.middleware.session import DiskSessionStore, SessionMiddleware
import yaro
import selector
import barrel.basic, barrel.form, barrel.roles
from paste.exceptions.errormiddleware import ErrorMiddleware
from paste.debug.profile import ProfileMiddleware as _ProfileMiddleware
from paste.debug.prints import PrintDebugMiddleware

session_key = 'com.saddi.service.session'

######## Controller Related ##########

wsgize = yaro.Yaro
URLs = selector.Selector

######## Authentication ###########

valid_user = lambda username, password: False
user_roles = lambda username: ()
login_path = '/login'

class LoginAuth(barrel.form.FormAuth):
    user_field = 'username'
    pass_field = 'password'
    url_field = 'previous_url'
    button = 'login'
    environ_user_key = 'meilanweb.auth.username'

    def valid_user(self, username, password):
        return valid_user(username, password)

    def not_authenticated(self, environ, start_response):
        sio = environ['wsgi.input']
        sio.seek(0)
        fs = cgi.FieldStorage(fp=sio, environ=environ, keep_blank_values=True)
        if fs.getlist(self.button):
            try:
                previous_url = fs[self.url_field].value
                environ['meilanweb.auth.previous_url'] = previous_url
            except KeyError:
                pass
        return login_form(environ, start_response)
login_auth = LoginAuth

class LogoutAuth(LoginAuth):
    def __call__(self, environ, start_response):
        session = self.session_dict(environ)
        try:
            del session[self.session_user_key]
        except (TypeError, KeyError):
            pass
        return self.app(environ, start_response)
logout_auth = LogoutAuth

class ForwardAuth(LoginAuth):
    def username_and_password(self, environ):
        return '', ''

    def not_authenticated(self, environ, start_response):
        environ['meilanweb.auth.previous_url'] = request_uri(environ)
        return login_form(environ, start_response)

class MyRoles(barrel.roles.RolesAuthz):
    def user_roles(self, username):
        return user_roles(username)

def auth(*allowed_roles):
    def deco(app):
        return ForwardAuth(MyRoles(app, allowed_roles=allowed_roles))
    return deco

def login_form(environ, start_response):
    if environ.has_key('meilanweb.auth.previous_url'):
        start_response('403 Forbidden', [('Content-Type', 'text/html')])
    else:
        start_response('200 OK', [('Content-Type', 'text/html')])
    action = login_path
    username = environ.get('meilanweb.auth.username', '')
    if username:
        message = "Sign in failed; please try again"
    else:
        message = "Please enter your username and password"
    previous_url = cgi.escape(environ.get('meilanweb.auth.previous_url')
            or environ.get('HTTP_REFERER', '/'))
    return ["""<html><head><title>Login</title></head>
<body>
<h1>Resource Requires Authentication</h1>
<form method="post" action="%(action)s">
<fieldset>
<legend>%(message)s</legend>
<label for="username">Username:</label>
<input type="text" id="username" name="username" value="%(username)s" />
<br />
<label for="password">Password:</label>
<input type="password" id="password" name="password" />
<br />
<button type="submit" name="login" id="login" value="submit">Sign In</button>
<input type="hidden" name="previous_url" value="%(previous_url)s" />
</fieldset>
</form></body></html> """ % locals()]

@login_auth
@wsgize
def login(req):
    previous_url = req.form.get('previous_url', '/')
    return req.redirect(previous_url)

@logout_auth
@wsgize
def logout(req):
    return "You have logged out."


######## Templating Related ##########

loader = TemplateLoader(['templates'])
template_vars = {}

def set_global_template_vars(**kwargs):
    # keys that startswith('_') will be filtered out.
    template_vars.update(dict((k, v) for k, v in kwargs.iteritems()
        if not k.startswith('_')))

def render(template_file, **kwargs):
    template = loader.load(template_file)
    kwargs.update(template_vars)
    stream = template.generate(**kwargs)
    body = stream.render('xhtml')
    return body

def expose(template_file):
    def deco(func):
        @wraps(func)
        def _(req):
            try:
                d = {'session': req.environ.get(session_key).session}
            except AttributeError:
                d = {'session': None}
            d.update(func(req))
            return render(template_file, **d)
        return _
    return deco

####### Server Related ############

def parse_args():
    from optparse import OptionParser
    parser = OptionParser()
    parser.add_option('-m', '--mode', type='choice',
            choices=('standalone', 'scgi'),
            default='standalone',
            help="run mode, standalone or scgi. [default: standalone]")
    parser.add_option('-p', '--port', type='int',
            help="listen port. [default: standalone: 8080, scgi: 4000]")
    parser.add_option('-i', '--interface', default='localhost',
            help="listen interface. [default: localhost]")
    return parser.parse_args()
 
def run(app, fvars={}, debug=False, profile=False, session=None,
        middlewares=()):
    options, args = parse_args()
   
    if options.port is None:
        if options.mode == 'scgi':
            options.port = 4000
        else:
            options.port = 8080
    options.address = (options.interface, options.port)

    if isinstance(app, selector.Selector):
        app.add('/static/{:any}', GET=static.Cling(os.getcwd()))
    else:
        s = selector.Selector()
        s.add('/static/{:any}', GET=static.Cling(os.getcwd()))
        s.add('{:any}', GET=app, POST=app, PUT=app, DELETE=app)
        app = s

    if debug:
        modname = os.path.splitext(os.path.basename(fvars['__file__']))[0]
        for k, v in fvars.iteritems():
            if v is app:
                appname = k
                break
        else:
            appname = None
        app = ReloadMiddleware(app, modname, appname)

    for mw in middlewares:
        app = mw(app)

    if profile:
        app = ProfileMiddleware(app)

    if session is not None:
        app = session(app)

    app = ErrorMiddleware(app, debug=debug)

    if debug:
        app = PrintDebugMiddleware(app)

    if options.mode == 'scgi':
        return run_scgi(app, options.address)
    return run_simple(app, options.address)

def run_autoload(resolvable, targets, exceptions=None):
    app = memento.Assassin(resolvable, targets, exceptions)
    run(app)

def run_simple(app, addr=('localhost', 8080)):
    from wsgiref.simple_server import make_server
    httpd = make_server(addr[0], addr[1], app)
    print "Serving HTTP on %s port %s ..." % httpd.socket.getsockname()
    httpd.serve_forever()

def run_scgi(app, addr=('localhost', 4000)):
    from flup.server.scgi import WSGIServer
    return WSGIServer(app, bindAddress=addr).run()

############# Middlewares ###################

def show_environ(app):
    @wraps(app)
    def _(environ, start_response):
        x = app(environ, start_response)
        def g():
            for c in x:
                yield c
            yield "<hr>"
            yield """<table border="1">"""
            yield """<caption><em>WSGI Environ</em></caption>"""
            for k, v in sorted(environ.iteritems()):
                yield """<tr><th>%s</th><td>%s</td></tr>""" % (k, v)
            yield "</table>"
        return g()
    return _

def session(app, *args, **kwargs):
    store = DiskSessionStore(*args, **kwargs)
    def cleanup():
        while True:
            store.periodic()
            try:
                time.sleep(300)
            except AttributeError:
                break
    t = threading.Thread(target=cleanup)
    t.setDaemon(True)
    t.start()
    return SessionMiddleware(store, app)

################ Debug Middlewares #####################

class ReloadMiddleware:
    def __init__(self, app, modname, appname):
        self.app = app
        self.modname = modname
        self.mod = __import__(modname)
        self.appname = appname
        self.mtimes = {}

    def check(self):
        for mod in sys.modules.values():
            try:
                mtime = os.stat(mod.__file__).st_mtime
            except (AttributeError, OSError, IOError):
                continue
            if mod.__file__.endswith('.pyc') and \
                    os.path.exists(mod.__file__[:-1]):
                mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
            if mod not in self.mtimes:
                self.mtimes[mod] = mtime
            elif self.mtimes[mod] < mtime:
                try:
                    if mod is self.mod:
                        self.mod = reload(mod)
                        self.app = getattr(mod, self.appname)
                    else:
                        reload(mod)
                except ImportError:
                    pass

    def __call__(self, environ, start_response):
        self.check()
        return self.app(environ, start_response)

if sys.hexversion >= 0x2050000:
    import hotshot
    import hotshot.stats
    from cStringIO import StringIO
    from paste import response
    class ProfileMiddleware(_ProfileMiddleware):
        def __call__(self, environ, start_response):
            catch_response = []
            body = []
            def replace_start_response(status, headers, exc_info=None):
                catch_response.extend([status, headers])
                start_response(status, headers, exc_info)
                return body.append
            def run_app():
                body.extend(self.app(environ, replace_start_response))
            self.lock.acquire()
            try:
                prof = hotshot.Profile(self.log_filename)
                prof.addinfo('URL', environ.get('PATH_INFO', ''))
                try:
                    prof.runcall(run_app)
                finally:
                    prof.close()
                body = ''.join(body)
                headers = catch_response[1]
                content_type = response.header_value(headers, 'content-type')
                if not (content_type and content_type.startswith('text/html')):
                    # We can't add info to non-HTML output
                    return [body]
                output = StringIO()
                stats = hotshot.stats.load(self.log_filename)
                stats.stream = output
                stats.strip_dirs()
                stats.sort_stats('time', 'calls')
                stats.print_stats(self.limit)
                stats.print_callers(self.limit)
                output = output.getvalue()
                body += '<pre style="%s">%s</pre>' % (
                    self.style, cgi.escape(output))
                return [body]
            finally:
                self.lock.release()

else:
    ProfileMiddleware = _ProfileMiddleware
